﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.CommandBars;
using EnvDTE;
using EnvDTE80;
using System.Reflection;

namespace Highlighterr
{

    [Export(typeof(IClassifierProvider))]
    [ContentType("C/C++")] //only for C/C++ code...
    internal class HighlighterrProvider : IClassifierProvider
    {
        //this class seems to be instantiated and destroyed with the IDE when 
        //it loads and unloads, so should be safe to store shared data in here.

        [Import]
        internal IClassificationTypeRegistryService ClassificationRegistry = null; //Set via MEF
        [Import]
        internal SVsServiceProvider ServiceProvider = null; //Set via MEF
        [Import]
        internal ITextEditorFactoryService TextEditorService = null; //Set via MEF


        //references that are used when creating the classifiers
        private DTE2 _Dte2 = null;
        private Intellisenser _Intellisenser = null;
        private string _SolutionName = string.Empty;

        //various event handlers, which are used in the (test) completion code
        private TextDocumentKeyPressEvents _TextDocKeyEvents = null;
        private _dispTextDocumentKeyPressEvents_BeforeKeyPressEventHandler _KeyBeforeHandler;
        private _dispTextDocumentKeyPressEvents_AfterKeyPressEventHandler _KeyAfterHandler;
        private EventHandler<TextViewCreatedEventArgs> _TvEventHandler;
        
        //objects which might be used for something to do with completion lists...
        private List<ITextView> _AllTextViews = new List<ITextView>();
        private ITextView _ITextView = null;

        private CommandBar _CodeWindowCommandBar = null;
        private CommandBarPopup _HighlighterrPopup = null;
        //private CommandBarControl _RenamePopupButton = null;
        //private CommandBarEvents _RenamePopupEvents = null;
        private CommandBarControl _RescanPopupButton = null;
        private CommandBarEvents _RescanPopupEvents = null;
        private CommandBarControl _SwitchPopupButton = null;
        private CommandBarEvents _SwitchPopupEvents = null;
        private CommandBarControl _StatsPopupButton = null;
        private CommandBarEvents _StatsPopupEvents = null;


        private StatisticsForm _StatsForm = null;

        private string _SourceFileName = string.Empty;
        private string _AltSourceFileName = string.Empty;


        public IClassifier GetClassifier(ITextBuffer buffer)
        {
            //this function is called by VS to create classifiers
            //seemingly called when a document is first loaded

            //get a reference to the main VS extensibility object (that's what it is, right?)
            _Dte2 = (DTE2)ServiceProvider.GetService(typeof(DTE));


            EnsureIntellisenser(); //make sure the intellisenser is ok

            //EnsureKeyHandler(); //attach to key events for completion list (not used atm)


            EnsureHighlighterrMenuItems();


            //_SourceFileName = HighlighterrUtility.GetBufferFileName(buffer);
            //UpdateSwitchMenuItem();


            //the main purpose of this function is to create the IClassifier
            return buffer.Properties.GetOrCreateSingletonProperty<Highlighterr>(delegate { return new Highlighterr(ClassificationRegistry, _Intellisenser, buffer); });
        }



        private void EnsureHighlighterrMenuItems()
        {
            if (_CodeWindowCommandBar == null)
            {
                _CodeWindowCommandBar = _Dte2.CommandBars["Code Window"];
                
                if (_CodeWindowCommandBar != null)
                {
                    _HighlighterrPopup = (CommandBarPopup)_CodeWindowCommandBar.Controls.Add(MsoControlType.msoControlPopup, Missing.Value, Missing.Value, 1, true);
                    _HighlighterrPopup.Caption = "Highlighterr";


                    //_RenamePopupButton = _HighlighterrPopup.Controls.Add(MsoControlType.msoControlButton, Missing.Value, Missing.Value, Missing.Value, true);
                    //_RenamePopupButton.Caption = "Rename... (under development!)";
                    //_RenamePopupButton.BeginGroup = true;
                    //_RenamePopupEvents = _Dte2.Events.get_CommandBarEvents(_RenamePopupButton);
                    //_RenamePopupEvents.Click += new _dispCommandBarControlEvents_ClickEventHandler(RenamePopup_Click); ;

                    _RescanPopupButton = _HighlighterrPopup.Controls.Add(MsoControlType.msoControlButton, Missing.Value, Missing.Value, Missing.Value, true);
                    _RescanPopupButton.Caption = "Rescan SDF";
                    _RescanPopupButton.BeginGroup = true;
                    _RescanPopupEvents = _Dte2.Events.get_CommandBarEvents(_RescanPopupButton);
                    _RescanPopupEvents.Click += new _dispCommandBarControlEvents_ClickEventHandler(RescanPopup_Click);

                    _SwitchPopupButton = _HighlighterrPopup.Controls.Add(MsoControlType.msoControlButton, Missing.Value, Missing.Value, Missing.Value, true);
                    _SwitchPopupButton.BeginGroup = true;
                    _SwitchPopupButton.Caption = "Switch to...";
                    _SwitchPopupButton.Enabled = false;
                    _SwitchPopupEvents = _Dte2.Events.get_CommandBarEvents(_SwitchPopupButton);
                    _SwitchPopupEvents.Click += new _dispCommandBarControlEvents_ClickEventHandler(SwitchPopup_Click);

                    _StatsPopupButton = _HighlighterrPopup.Controls.Add(MsoControlType.msoControlButton, Missing.Value, Missing.Value, Missing.Value, true);
                    _StatsPopupButton.Caption = "Statistics...";
                    _StatsPopupButton.BeginGroup = true;
                    _StatsPopupEvents = _Dte2.Events.get_CommandBarEvents(_StatsPopupButton);
                    _StatsPopupEvents.Click += new _dispCommandBarControlEvents_ClickEventHandler(StatsPopup_Click);


                    //try
                    //{
                    //    int nidx = _RenamePopupButton.Index + 1;
                    //    _CodeWindowCommandBar.Controls[nidx].BeginGroup = true;
                    //}
                    //catch { }

                }
                
                _Dte2.Events.WindowEvents.WindowActivated += new _dispWindowEvents_WindowActivatedEventHandler(WindowEvents_WindowActivated);
            
            }


        }

        private void EnsureIntellisenser()
        {
            //recreate intellisensers if solution changed
            if (_Dte2.Solution.FullName != _SolutionName)
            {
                _Intellisenser = null;
                _SolutionName = _Dte2.Solution.FullName;
            }

            //create an intellisenser if there isn't one
            if (_Intellisenser == null)
            {
                _Intellisenser = new Intellisenser(_Dte2);
                
                if (_StatsForm != null)
                {
                    _StatsForm.SetIntellisenser(_Intellisenser);
                }
            }

        }

        private void EnsureKeyHandler()
        {
            //attach events, detach if they're already attached.
            //this probably has some issues, but mostly works...

            Events2 events2 = (Events2)_Dte2.Events;
            if (_TextDocKeyEvents == null)
            {
                _KeyBeforeHandler = new _dispTextDocumentKeyPressEvents_BeforeKeyPressEventHandler(BeforeKeyPress);
                _KeyAfterHandler = new _dispTextDocumentKeyPressEvents_AfterKeyPressEventHandler(AfterKeyPress);
                _TvEventHandler = new EventHandler<TextViewCreatedEventArgs>(TextEditorService_TextViewCreated);
            }
            else
            {
                _AllTextViews.Clear();
                _TextDocKeyEvents.BeforeKeyPress -= _KeyBeforeHandler;
                _TextDocKeyEvents.AfterKeyPress -= _KeyAfterHandler;
                TextEditorService.TextViewCreated -= _TvEventHandler;
            }
            _TextDocKeyEvents = events2.get_TextDocumentKeyPressEvents(null);
            _TextDocKeyEvents.BeforeKeyPress += _KeyBeforeHandler;
            _TextDocKeyEvents.AfterKeyPress += _KeyAfterHandler;
            TextEditorService.TextViewCreated += _TvEventHandler;

        }



        private void WindowEvents_WindowActivated(Window GotFocus, Window LostFocus)
        {
            Document docgotfocus = GotFocus.Document;
            if (docgotfocus != null)
            {
                string newfile = docgotfocus.FullName;
                if (newfile != _SourceFileName)
                {
                    _SourceFileName = newfile;
                    if (_SwitchPopupButton != null)
                    {
                        string filepath = System.IO.Path.GetDirectoryName(_SourceFileName);
                        string filename = System.IO.Path.GetFileNameWithoutExtension(_SourceFileName);
                        string extension = System.IO.Path.GetExtension(_SourceFileName).ToLower();
                        string altextension = extension == ".h" ? ".cpp" : extension == ".cpp" ? ".h" : string.Empty;
                        string altfilename = filename + altextension;
                        _AltSourceFileName = filepath + System.IO.Path.DirectorySeparatorChar + altfilename;
                        bool exists = System.IO.File.Exists(_AltSourceFileName);
                        _SwitchPopupButton.Caption = !string.IsNullOrEmpty(altextension) ? string.Format("Switch to {0}", altfilename) : "Switch to...";
                        _SwitchPopupButton.Enabled = exists;
                    }
                }
            }
        }


        void BeforeKeyPress(string Keypress, TextSelection Selection, bool InStatementCompletion, ref bool CancelKeypress)
        {
            //was having a go at auto-displaying the completion list, but couldn't get it to work how
            //i wanted without having code snippets for things like if(), for() etc, in c++.
            //specifically, typing if( selected something else on the list, which was annoying.
            //if if(), for() etc were code snippets, then it would work well, as c# does.
            
            if (InStatementCompletion)
            {
                int ik = (int)Keypress[0];
                if (ik < 255)
                {
                    if (_Intellisenser.IsIdentifierChar[ik])
                    {
                        //show the list if an identifier character was typed..
                        //dte2.ExecuteCommand("Edit.SelectionCancel");
                        _Dte2.ExecuteCommand("Edit.ListMembers"); //this command shows the completion list.
                    }
                    else
                    {
                        if ((ik == 13) || (ik == 32) || (ik == 46) || (ik == 43) || (ik == 45) || (ik == 61))
                        {
                            //return, space, period, minus, plus, equals
                            //not doing anything here accepts the currently selected item..
                        }
                        else
                        {
                            //otherwise hide the list
                            _Dte2.ExecuteCommand("Edit.SelectionCancel");
                        }
                    }
                }
            }

        }
        void AfterKeyPress(string Keypress, TextSelection Selection, bool InStatementCompletion)
        {
        }


        private void TextEditorService_TextViewCreated(object sender, TextViewCreatedEventArgs e)
        {
            //these are just stored for later use.
            _AllTextViews.Add(e.TextView);
            _ITextView = e.TextView;
        }



        private void RenamePopup_Click(object CommandBarControl, ref bool Handled, ref bool CancelDefault)
        {
            RenameForm frm = new RenameForm(_Intellisenser, _Dte2.ActiveDocument);
            if (!frm.IsDisposed)//could be disposed if name wasn't found
            {
                frm.ShowDialog();
            }
        }
        private void RescanPopup_Click(object CommandBarControl, ref bool Handled, ref bool CancelDefault)
        {
            if (_Intellisenser != null)
            {
                _Intellisenser.ReloadSolutionItems();
            }
        }
        private void SwitchPopup_Click(object CommandBarControl, ref bool Handled, ref bool CancelDefault)
        {
            if (System.IO.File.Exists(_AltSourceFileName))
            {
                _Dte2.ItemOperations.OpenFile(_AltSourceFileName);
            }
        }
        private void StatsPopup_Click(object CommandBarControl, ref bool Handled, ref bool CancelDefault)
        {
            if ((_StatsForm == null) || (_StatsForm.IsDisposed))
            {
                _StatsForm = new StatisticsForm();
            }

            _StatsForm.SetIntellisenser(_Intellisenser);

            _StatsForm.Show();

        }



    }

    class Highlighterr : IClassifier
    {
        //this is the main classifier class. created seemingly on a per-document basis as needed by VS.

        public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;

        private IClassificationType _ClassificationSomething;//only used as a fallback...
        private IClassificationType _ClassificationFunction;
        private IClassificationType _ClassificationType;
        private IClassificationType _ClassificationTypedef;
        private IClassificationType _ClassificationEnum;
        private IClassificationType _ClassificationMacro;

        private Intellisenser _Intellisenser;

        private ITextBuffer _TextBuffer;
        private Type _CppColorerType;
        private object _CppColorer = null;
        private MethodInfo _CppColorMethod = null;

        internal Highlighterr(IClassificationTypeRegistryService registry, Intellisenser intelli, ITextBuffer buffer)
        {
            //get all the classifications for use when getting the spans
            _ClassificationSomething = registry.GetClassificationType("HighlighterrSomething");
            _ClassificationType = registry.GetClassificationType("HighlighterrType");
            _ClassificationTypedef = registry.GetClassificationType("HighlighterrTypedef");
            _ClassificationFunction = registry.GetClassificationType("HighlighterrFunction");
            _ClassificationEnum = registry.GetClassificationType("HighlighterrEnum");
            _ClassificationMacro = registry.GetClassificationType("HighlighterrMacro");

            _CppColorerType = Type.GetType("Microsoft.VisualC.CppColorer, Microsoft.VisualC.Editor.Implementation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
            
            _Intellisenser = intelli;
            _TextBuffer = buffer;


            if (ClassificationChanged != null) { }//this just gets rid of that silly warning without needing #pragma warning disable 67...

        }


        private bool GetMultilineCommentStatus(int lineNumber)
        {
            bool r = false;
            if (_CppColorer == null)
            {
                if (_TextBuffer.Properties.TryGetProperty(_CppColorerType, out _CppColorer))
                {
                    _CppColorMethod = _CppColorerType.GetMethod("GetColorStateAtStartOfLine");
                }
            }
            if (_CppColorMethod != null)
            {
                object rval = _CppColorMethod.Invoke(_CppColorer, new object[] { lineNumber });
                if (rval != null)
                {
                    int ival = (int)rval;
                    r = ival != 0;
                }
            }

            return r;
        }

        public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
        {
            //this is the main "entry point" for the highlighter, called by VS for text that
            //needs to be 'classified'.


            //create a list to hold the results
            List<ClassificationSpan> classifications = new List<ClassificationSpan>();


            //don't classify anything if the intellisenser didn't initialise,
            //usually due to a non-vc++ project i guess..
            if (_Intellisenser == null) return classifications;

            string file = HighlighterrUtility.GetBufferFileName(span.Snapshot.TextBuffer);

            string s = span.GetText(); //main string for this span
            bool inword = false;
            bool wasinword = false;
            int wordstart = 0;
            bool commentstart = false;
            bool incomment = false;//this handles double-slash comments
            bool endmultiline = false;//this is for handling /* */ comments
            bool instring = false;
            char stringterminator = '"';
            int multilinestarti = 0;
            bool inmultiline = GetMultilineCommentStatus(span.Snapshot.GetLineNumberFromPosition(span.Start.Position));

            //go through the string and find words that are in the intellisenser dictionaries...
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];
                int ic = (int)c;
                wasinword = inword;
                inword = false;

                if (c == '/' && !instring)
                {
                    if (commentstart)
                    {
                        incomment = true; //inside a comment if this and the last char are /
                    }
                    commentstart = true; //needs to wait for a second slash
                    if (endmultiline)
                    {
                        endmultiline = false;//last char was *, so not in multi line comment anymore.
                        inmultiline = false;
                    }
                }
                else
                {
                    if (c == '*' && !instring)
                    {
                        if (inmultiline)
                        {
                            endmultiline = true; //not in a multi liner if the next char is /
                        }
                        else if (commentstart && !incomment)
                        {
                            inmultiline = true; //mark as in multi line, unless already in a comment
                            multilinestarti = span.Start + i; //and mark where it started
                        }
                    }
                    else
                    {
                        endmultiline = false;//no multiline comment end..
                        if (!inmultiline && !incomment) //handle strings, only outside of comments
                        {
                            if (instring)
                            {
                                if (c == stringterminator) //end a string if found the terminator
                                {
                                    instring = false;
                                }
                            }
                            else //test for string opening chars
                            {
                                if (c == '"')
                                {
                                    instring = true;
                                    stringterminator = c;
                                }
                                if (c == '\'')
                                {
                                    instring = true;
                                    stringterminator = c;
                                }
                            }
                        }
                    }
                    commentstart = false; //no comment..
                }
                if (c == '\r' || c == '\n') //not in a single-line comment anymore.
                {
                    commentstart = false;
                    incomment = false;
                }

                //only try highlighting if the current char isn't in a comment or a string.
                if (!incomment && !inmultiline && !instring)
                {
                    bool islastchar = (i + 1) == s.Length;
                    if (ic < 256)
                    {
                        inword = _Intellisenser.IsIdentifierChar[ic]; //quickly determine if this char is part of a word
                    }
                    if (inword && !wasinword)
                    {
                        wordstart = i; //last char wasn't an identifier char, so a word just started.
                    }
                    else if (wasinword && (!inword || islastchar)) //this char isn't an identifier, but the last one was, so a word just ended...
                    {
                        int wordlength = i - wordstart;
                        if (islastchar) wordlength++;
                        string word = s.Substring(wordstart, wordlength);

                        //check to see if the word is in the intellisenser...
                        IntellisenserItem item = _Intellisenser.Find(word);

                        //set the classification type based on what (if anything) was found in the intellisenser.
                        if (item != null)
                        {
                            IClassificationType classtype;
                            switch (item.Type)
                            {
                                default:
                                case IntellisenserType.Something:
                                    classtype = _ClassificationSomething;
                                    break;
                                case IntellisenserType.Function:
                                    classtype = _ClassificationFunction;
                                    break;
                                case IntellisenserType.Type:
                                    classtype = _ClassificationType;
                                    break;
                                case IntellisenserType.Typedef:
                                    classtype = _ClassificationTypedef;
                                    break;
                                case IntellisenserType.Enum:
                                    classtype = _ClassificationEnum;
                                    break;
                                case IntellisenserType.Macro:
                                    classtype = _ClassificationMacro;
                                    break;
                            }
                            //create the classification span and add it to the list.
                            classifications.Add(new ClassificationSpan(new SnapshotSpan(span.Snapshot, new Span(span.Start + wordstart, wordlength)), classtype));
                        }
                    }
                }
            }



            return classifications;
        }


    }
}
